<?php

/**
 * 一款简单的防火墙插件
 * @package TypechoFirewall
 * @author 凌清云
 * @version 1.0
 * @link https://www.qynest.cn
 */

// 设置中国时区
date_default_timezone_set('Asia/Shanghai');
class TypechoFirewall_Plugin implements Typecho_Plugin_Interface
{
    // 日志文件路径（修改为绝对路径）
    const LOG_FILE = __DIR__ . '/logs/access.log';
    const BLOCKED_LOG = __DIR__ . '/logs/blocked.log';
    const BLACKLIST_FILE = __DIR__ . '/data/blacklist.txt';
    const WHITELIST_FILE = __DIR__ . '/data/whitelist.txt';

    // 插件激活接口
    public static function activate()
    {
        // 创建目录结构
        $dirs = [__DIR__ . '/logs', __DIR__ . '/data'];
        foreach ($dirs as $dir) {
            if (!file_exists($dir)) {
                mkdir($dir, 0755, true);
            }
        }
        
        // 初始化文件
        self::initLogFiles();

        // 创建必要文件
        if (!file_exists(self::BLACKLIST_FILE)) {
            file_put_contents(self::BLACKLIST_FILE, "# 每行一个黑名单IP\n# 示例: 192.168.1.100\n# 支持通配符: 192.168.*\n");
        }

        if (!file_exists(self::WHITELIST_FILE)) {
        file_put_contents(self::WHITELIST_FILE, "# 每行一个白名单IP\n# 示例: 192.168.1.100\n# 支持通配符: 192.168.*\n");
        }
        
        // 挂载钩子
        Typecho_Plugin::factory('index.php')->begin = array(__CLASS__, 'firewallCheck');
        Typecho_Plugin::factory('admin/menu.php')->navBar = array(__CLASS__, 'renderAdminNotice');
        
        // 添加左侧菜单
        Helper::addPanel(1, 'TypechoFirewall/manage.php', '防火墙管理', '防火墙管理', 'administrator');
        
        return _t('防火墙插件已激活，请配置规则并查看管理面板');
    }

    private static function initLogFiles()
    {
        $files = [
            self::LOG_FILE,
            self::BLOCKED_LOG,
            self::BLACKLIST_FILE,
            self::WHITELIST_FILE
        ];
        
        foreach ($files as $file) {
            if (!file_exists($file)) {
                @file_put_contents($file, '');
                @chmod($file, 0666);
            }
        }
    }

    public static function deactivate()
    {
        Helper::removeRoute('firewall-manage');
        Helper::removePanel(1, 'TypechoFirewall/manage.php');
        return _t('防火墙插件已禁用');
    }

    // 插件配置面板
    public static function config(Typecho_Widget_Helper_Form $form)
    {
        // 防护开关
        $blockSql = new Typecho_Widget_Helper_Form_Element_Radio(
            'block_sql', 
            array('1' => '开启', '0' => '关闭'),
            '1',
            'SQL注入防护',
            '拦截常见的SQL注入攻击语句'
        );
        $form->addInput($blockSql);

        $blockXss = new Typecho_Widget_Helper_Form_Element_Radio(
            'block_xss', 
            array('1' => '开启', '0' => '关闭'),
            '1',
            'XSS攻击防护',
            '拦截跨站脚本攻击'
        );
        $form->addInput($blockXss);

        $blockShell = new Typecho_Widget_Helper_Form_Element_Radio(
            'block_shell', 
            array('1' => '开启', '0' => '关闭'),
            '1',
            '木马防护',
            '拦截常见的一句话木马特征'
        );
        $form->addInput($blockShell);

        $blockCrawler = new Typecho_Widget_Helper_Form_Element_Radio(
            'block_crawler', 
            array('1' => '开启', '0' => '关闭'),
            '1',
            '防采集',
            '阻止高频访问的采集程序'
        );
        $form->addInput($blockCrawler);
        
        $enableLog = new Typecho_Widget_Helper_Form_Element_Radio(
            'enable_log', 
            array('1' => '开启', '0' => '关闭'),
            '1',
            '访问日志',
            '记录所有访问请求'
        );
        $form->addInput($enableLog);
        
        $autoBlock = new Typecho_Widget_Helper_Form_Element_Radio(
            'auto_block', 
            array('1' => '开启', '0' => '关闭'),
            '1',
            '自动拉黑',
            '拦截超过5次的恶意IP'
        );
        $form->addInput($autoBlock);
    }

    // 用户配置处理
    public static function personalConfig(Typecho_Widget_Helper_Form $form) {}

    // 核心防火墙逻辑
    public static function firewallCheck()
    {
        $requestUri = $_SERVER['REQUEST_URI'] ?? '';
        $isAdminRequest = strpos($requestUri, '/admin/') !== false;
        
        // 跳过对管理请求的检查
        if ($isAdminRequest) {
            return;
        }
        
        $ip = self::getClientIP();
        // 白名单IP直接放行
        if (self::isWhitelisted($ip)) {
            return;
        }

        // 跳过黑名单操作请求
        if (isset($_GET['action']) && $_GET['action'] === 'remove' && isset($_GET['ip'])) {
            return;
        }

        $options = Helper::options()->plugin('TypechoFirewall');
        $ip = self::getClientIP();
        
        // 先检查黑名单
        if (self::isBlacklisted($ip)) {
            self::blockRequest("黑名单IP: {$ip}", 403, true);
            exit;
        }
        
        // 记录访问日志
        if ($options->enable_log) {
            $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
            $method = $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN';
            self::logAccess($ip, $method, $requestUri, $ua);
        }
        
        // 防采集检测
        if ($options->block_crawler && self::isHighFrequency($ip)) {
            if ($options->auto_block) {
                self::addToBlacklist($ip, "高频访问");
            }
            self::blockRequest("访问频率过高: $ip", 429, true);
        }
        
        // 检查所有输入数据
        $inputs = array_merge($_GET, $_POST, $_COOKIE);
        foreach ($inputs as $key => $value) {
            if (is_array($value)) continue;
            
            // SQL注入检测
            if ($options->block_sql && self::detectSqlInjection($value)) {
                if ($options->auto_block) {
                    self::addToBlacklist($ip, "SQL注入攻击");
                }
                self::blockRequest("检测到SQL注入: $key", 403, true);
            }
            
            // XSS攻击检测
            if ($options->block_xss && self::detectXSS($value)) {
                if ($options->auto_block) {
                    self::addToBlacklist($ip, "XSS攻击");
                }
                self::blockRequest("检测到XSS攻击: $key", 403, true);
            }
            
            // 木马特征检测
            if ($options->block_shell && self::detectShell($value)) {
                if ($options->auto_block) {
                    self::addToBlacklist($ip, "木马攻击");
                }
                self::blockRequest("检测到木马特征: $key", 403, true);
            }
        }
    }
    
    // 在后台显示通知
    public static function renderAdminNotice()
    {
        //echo '<span class="firewall-status" style="
        //    display: inline-block;
        //    padding: 8px 12px;
        //    background: #4CAF50;
        //    color: white;
        //    border-radius: 3px;
        //    font-size: 14px;
        //">防火墙状态：已开启</span>';
    }

    // 获取客户端真实IP
    private static function getClientIP()
    {
        $ip = $_SERVER['REMOTE_ADDR'];
        
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
            $ip = $_SERVER['HTTP_CLIENT_IP'];
        } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
        }
        
        return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : $_SERVER['REMOTE_ADDR'];
    }

    // SQL注入检测
    private static function detectSqlInjection($input)
    {
        $patterns = array(
            '/(\bunion\b\s*\bselect\b)/i',
            '/\b(?:drop|delete|truncate|alter)\s+table\b/i',
            '/\b(?:update|insert)\s+.+?\bset\b/i',
            '/\b(?:sleep|benchmark)\(/i',
            '/\b(?:select|create|exec|declare)\b/i',
            '/(?:--|#|\/\*).*?\n/',
            '/\b(?:xp_cmdshell|load_file)\b/i'
        );
        return preg_match(implode('|', $patterns), $input);
    }

    // XSS攻击检测
    private static function detectXSS($input)
    {
        return preg_match('/(<script|javascript:|onmouseover=|onload=|eval\(|fromCharCode|alert\(|document\.cookie)/i', $input);
    }

    // 木马特征检测
    private static function detectShell($input)
    {
        $signatures = array(
            '@eval\(@',
            '@base64_decode\(@',
            '@gzinflate\(@',
            '@system\(@',
            '@shell_exec\(@',
            '@assert\(@',
            '@create_function\(@',
            '@call_user_func\(@',
            '@file_put_contents\(@',
            '/<\?php\s+eval\b/i',
            '/<\?php\s+assert\b/i',
            '/`\s*\$_/'
        );
        return preg_match(implode('|', $signatures), $input);
    }

    // 高频访问检测
    private static function isHighFrequency($ip)
    {
        $cacheKey = 'firewall_freq_' . md5($ip);
        $cache = Typecho_Cookie::get($cacheKey);
        
        if ($cache) {
            list($lastTime, $count) = explode('|', $cache);
            if (time() - $lastTime < 60) {
                if ($count > 30) return true;
                Typecho_Cookie::set($cacheKey, time() . '|' . ($count + 1), 60);
            } else {
                Typecho_Cookie::set($cacheKey, time() . '|1', 60);
            }
        } else {
            Typecho_Cookie::set($cacheKey, time() . '|1', 60);
        }
        return false;
    }
    
    // 记录访问日志
    private static function logAccess($ip, $method, $uri, $ua)
    {
        try {
            $log = sprintf(
                "[%s] %s %s %s \"%s\"\n",
                date('Y-m-d H:i:s'),
                $ip,
                $method,
                $uri,
                addslashes(substr($ua, 0, 200))
            );
            
            $file = self::LOG_FILE;
            if (is_writable($file) || (!file_exists($file) && is_writable(dirname($file)))) {
                file_put_contents($file, $log, FILE_APPEND | LOCK_EX);
            } else {
                error_log("Firewall: Cannot write to log file {$file}");
            }
        } catch (Exception $e) {
            error_log("Firewall log error: " . $e->getMessage());
        }
    }
    
    // 检查IP是否在黑名单
    private static function isBlacklisted($ip)
    {
        static $blacklist = null;
        
        if ($blacklist === null) {
            $blacklist = self::getBlacklist();
        }
        
        // 精确匹配检查
        if (in_array($ip, $blacklist)) {
            return true;
        }
        
        // 通配符规则检查
        foreach ($blacklist as $rule) {
            if (strpos($rule, '*') !== false && self::matchWildcardRule($ip, $rule)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 匹配通配符规则
     * @param string $ip 待检查的IP
     * @param string $rule 通配符规则，如 192.168.*
     * @return bool
     */
    private static function matchWildcardRule($ip, $rule)
    {
        // 补全规则
        $segmentCount = substr_count($rule, '.') + 1;
        for ($i = $segmentCount; $i < 4; $i++) {
            $rule .= '.*';
        }
        
        $ruleSegments = explode('.', $rule);
        $ipSegments = explode('.', $ip);
        
        if (count($ipSegments) != 4 || count($ruleSegments) != 4) {
            return false;
        }
        
        for ($i = 0; $i < 4; $i++) {
            if ($ruleSegments[$i] !== '*' && $ruleSegments[$i] !== $ipSegments[$i]) {
                return false;
            }
        }
        
        return true;
    }
    
    // 获取黑名单列表
    public static function getBlacklist()
    {
        $file = self::BLACKLIST_FILE;
        if (!file_exists($file)) {
            return [];
        }
        
        $content = file_get_contents($file);
        if ($content === false) {
            error_log("Firewall: Cannot read blacklist file {$file}");
            return [];
        }
        
        $ips = [];
        foreach (explode("\n", $content) as $line) {
            $line = trim($line);
            if (empty($line) || $line[0] === '#') {
                continue;
            }
            
            $ip = preg_split('/\s+#/', $line)[0];
            $ip = trim($ip);
            
            if (filter_var($ip, FILTER_VALIDATE_IP) || self::validateWildcardRule($ip)) {
                $ips[] = $ip;
            }
        }
        
        return array_unique($ips);
    }

    /**
     * 验证通配符IP规则格式
     */
    private static function validateWildcardRule($ip)
    {
        $ip = strtolower(trim($ip));
        
        // 基础格式检查
        if (!preg_match('/^(\d{1,3}|\*)(\.(\d{1,3}|\*)){0,3}$/', $ip)) {
            return false;
        }
        
        // 补全缺失的段
        $segmentCount = substr_count($ip, '.') + 1;
        for ($i = $segmentCount; $i < 4; $i++) {
            $ip .= '.*';
        }
        
        // 验证每个段的合法性
        $segments = explode('.', $ip);
        foreach ($segments as $segment) {
            if ($segment !== '*' && ($segment < 0 || $segment > 255)) {
                return false;
            }
        }
        
        // 安全限制：通配符只能出现在最后1-2段
        $firstWildcardPos = array_search('*', $segments);
        if ($firstWildcardPos !== false && $firstWildcardPos < (count($segments) - 2)) {
            return false;
        }
        
        return true;
    }
    
    // 添加到黑名单
    public static function addToBlacklist($ip, $reason = '')
    {
        if (!self::isBlacklisted($ip)) {
            $log = "$ip # 自动添加: " . date('Y-m-d H:i:s') . " | 原因: $reason\n";
            file_put_contents(self::BLACKLIST_FILE, $log, FILE_APPEND);
            
            // 清除缓存
            self::clearBlacklistCache();
        }
    }
    
    // 从黑名单移除
    public static function removeFromBlacklist($ip)
    {
        $blacklist = self::getBlacklist();
        $newList = [];
        
        foreach ($blacklist as $item) {
            if ($item !== $ip && !self::matchWildcardRule($ip, $item)) {
                $newList[] = $item;
            }
        }
        
        $content = "# 防火墙黑名单\n" . implode("\n", $newList);
        file_put_contents(self::BLACKLIST_FILE, $content);
        
        // 清除缓存
        self::clearBlacklistCache();
    }
    
    // 清除黑名单缓存
    private static function clearBlacklistCache()
    {
        // 通过重置静态变量实现
        self::isBlacklisted(null);
    }
    
    // 获取最后拦截记录
    public static function getLastBlocked()
    {
        if (!file_exists(self::BLOCKED_LOG)) {
            return ['count' => 0, 'last' => '无'];
        }
        
        $lines = file(self::BLOCKED_LOG);
        $count = count($lines);
        $last = $count > 0 ? trim($lines[$count - 1]) : '无';
        
        return ['count' => $count, 'last' => $last];
    }

    // 拦截请求
    private static function blockRequest($reason, $code = 403, $log = false)
    {
        $ip = self::getClientIP();
        $uri = $_SERVER['REQUEST_URI'] ?? '/';
        $time = date('Y-m-d H:i:s');
        $currentYear = date('Y');
        $bin2hex_random = bin2hex(random_bytes(2));
        
        if ($log) {
            $logEntry = "[$time] [$ip] [$code] $reason | URI: $uri\n";
            @file_put_contents(self::BLOCKED_LOG, $logEntry, FILE_APPEND | LOCK_EX);
        }
        
        http_response_code($code);
        
        // 简体中文拦截页面
        echo <<<HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>请求被拦截 - 网站安全防护</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { 
            font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; 
            background: #f5f5f5; 
            color: #333; 
            line-height: 1.6;
        }
        .container {
            max-width: 800px;
            margin: 50px auto;
            padding: 30px;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #e74c3c;
            margin-bottom: 20px;
            padding-bottom: 15px;
            border-bottom: 1px solid #eee;
        }
        .alert {
            background: #fff8f8;
            border-left: 4px solid #e74c3c;
            padding: 15px;
            margin: 20px 0;
            border-radius: 0 4px 4px 0;
        }
        .info-grid {
            display: grid;
            grid-template-columns: 100px 1fr;
            gap: 10px;
            margin: 15px 0;
        }
        .info-label {
            font-weight: bold;
            color: #666;
        }
        .footer {
            margin-top: 30px;
            font-size: 14px;
            color: #999;
            text-align: center;
        }
        @media (max-width: 600px) {
            .container {
                margin: 20px;
                padding: 20px;
            }
            .info-grid {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🔐 访问请求已被拦截</h1>
        
        <div class="alert">
            <p>网站安全系统已阻止此次访问。如果这是误判，请联系网站管理员并提供下方信息。</p>
        </div>
        
        <div class="info-grid">
            <span class="info-label">拦截原因:</span>
            <span>{$reason}</span>
            
            <span class="info-label">您的IP:</span>
            <span>{$ip}</span>
            
            <span class="info-label">拦截时间:</span>
            <span>{$time}</span>
            
            <span class="info-label">访问地址:</span>
            <span>{$uri}</span>
        </div>
        
        <div class="footer">
            <p>Typecho 安全防火墙 &copy; {$currentYear} | 防护编号: FW-{$code}-{$bin2hex_random}</p>
        </div>
    </div>
</body>
</html>
HTML;
        exit;
    }

    /**
     * 获取最近的拦截记录
     * @param int $limit 获取数量
     * @return array
     */
    public static function getRecentBlocks($limit = 10)
    {
        if (!file_exists(self::BLOCKED_LOG)) {
            return [];
        }
        
        $lines = array_slice(file(self::BLOCKED_LOG), -$limit);
        $result = [];
        
        foreach ($lines as $line) {
            if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[(\d+)\] (.+)/', trim($line), $matches)) {
                $result[] = [
                    'time' => $matches[1],
                    'ip' => $matches[2],
                    'code' => $matches[3],
                    'reason' => $matches[4]
                ];
            }
        }
        
        return array_reverse($result);
    }

    /**
     * 清理日志文件
     * @param int $maxSize 最大文件大小(MB)
     */
    public static function rotateLogs($maxSize = 5)
    {
        $files = [self::LOG_FILE, self::BLOCKED_LOG];
        $maxBytes = $maxSize * 1024 * 1024;
        
        foreach ($files as $file) {
            if (file_exists($file) && filesize($file) > $maxBytes) {
                $backup = $file . '.' . date('YmdHis');
                rename($file, $backup);
                file_put_contents($file, '');
            }
        }
    }

    /**
     * 导出黑名单为文本
     * @return string
     */
    public static function exportBlacklist()
    {
        $blacklist = self::getBlacklist();
        return implode("\n", $blacklist);
    }

    /**
     * 批量导入黑名单
     * @param string $text 每行一个IP
     * @return int 成功导入数量
     */
    public static function importBlacklist($text)
    {
        $lines = explode("\n", $text);
        $count = 0;
        
        foreach ($lines as $line) {
            $line = trim($line);
            if (empty($line) || $line[0] === '#') {
                continue;
            }
            
            $ip = preg_split('/\s+#/', $line)[0];
            $ip = trim($ip);
            
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                self::addToBlacklist($ip, "批量导入");
                $count++;
            }
        }
        
        return $count;
    }


// 新增白名单检查方法
private static function isWhitelisted($ip)
{
    static $whitelist = null;
    
    if ($whitelist === null) {
        $whitelist = self::getWhitelist();
    }
    
    if (in_array($ip, $whitelist)) {
        return true;
    }
    
    // 通配符检查
    foreach ($whitelist as $rule) {
        if (strpos($rule, '*') !== false && self::matchWildcardRule($ip, $rule)) {
            return true;
        }
    }
    
    return false;
}
// 新增获取白名单方法
public static function getWhitelist()
{
    if (!file_exists(self::WHITELIST_FILE)) {
        return [];
    }
    
    $content = file_get_contents(self::WHITELIST_FILE);
    if ($content === false) {
        return [];
    }
    
    $ips = [];
    foreach (explode("\n", $content) as $line) {
        $line = trim($line);
        if (empty($line) || $line[0] === '#') continue;
        
        $ip = preg_split('/\s+#/', $line)[0];
        $ip = trim($ip);
        
        if (filter_var($ip, FILTER_VALIDATE_IP) || self::validateWildcardRule($ip)) {
            $ips[] = $ip;
        }
    }
    
    return array_unique($ips);
}
// 新增添加白名单方法
public static function addToWhitelist($ip, $reason = '')
{
    $ip = trim($ip);
    $whitelist = self::getWhitelist();
    
    if (!in_array($ip, $whitelist)) {
        $log = "$ip # 添加时间: " . date('Y-m-d H:i:s') . " | 原因: $reason\n";
        file_put_contents(self::WHITELIST_FILE, $log, FILE_APPEND);
        return true;
    }
    return false;
}
// 新增移除白名单方法
public static function removeFromWhitelist($ip)
{
    $whitelist = self::getWhitelist();
    $newList = [];
    
    foreach ($whitelist as $item) {
        if ($item !== $ip && !self::matchWildcardRule($ip, $item)) {
            $newList[] = $item;
        }
    }
    
    $content = "# 防火墙白名单\n" . implode("\n", $newList);
    file_put_contents(self::WHITELIST_FILE, $content);
    
    return count($whitelist) !== count($newList);
}

}
